/***************************************************************************
 * Copyright (C) 2006 Global Biodiversity Information Facility Secretariat.  
 * All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 ***************************************************************************/
package org.gbif.portal.webservices.rest;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.gbif.portal.util.path.PathMapping;
import org.gbif.portal.util.request.TemplateUtils;
import org.gbif.portal.webservices.actions.Action;
import org.gbif.portal.webservices.actions.DensityAction;
import org.gbif.portal.webservices.actions.DensityParameters;
import org.gbif.portal.webservices.actions.NetworkAction;
import org.gbif.portal.webservices.actions.NetworkParameters;
import org.gbif.portal.webservices.actions.OccurrenceAction;
import org.gbif.portal.webservices.actions.OccurrenceParameters;
import org.gbif.portal.webservices.actions.ProviderAction;
import org.gbif.portal.webservices.actions.ProviderParameters;
import org.gbif.portal.webservices.actions.ResourceAction;
import org.gbif.portal.webservices.actions.ResourceParameters;
import org.gbif.portal.webservices.actions.TaxonAction;
import org.gbif.portal.webservices.actions.TaxonParameters;
import org.gbif.portal.webservices.util.GbifWebServiceException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

/**
 * @author 
 * 
 * This class provide switching for REST requests. It reads the request value,
 * create parameter map and dispatch it to respective command object for
 * processing, get the result, preprocess if need be then send it back as
 * response
 * 
 * It uses naming conventions for request preprocessing, data access and
 * response preprocessing.
 * 
 * On receiving a request, it approves the request else returns an error doc
 * 
 */

public class Dispatcher implements Controller {

	public static Log log = LogFactory.getLog(Dispatcher.class);
	
	/** Suffix for KVP array values*/
	public static final String ARRAY_SUFFIX = "_array";

	/** Delimiter for outer value lists in the KVPs */
	protected static final String OUTER_DELIMITER = "()";

	/** Delimiter for inner value lists in the KVPs */
	protected static final String INNER_DELIMITER = ",";

	protected static final int OCCURRENCE_SERVICE = 1;

	protected static final int TAXON_SERVICE = 2;

	protected static final int NETWORK_SERVICE = 3;

	protected static final int WFS_SERVICE = 4;

	protected static final int PROVIDER_SERVICE = 5;

	protected static final int RESOURCE_SERVICE = 6;

	protected static final int DENSITY_SERVICE = 7;

	public static final int UNKNOWN_SERVICE = -1;

	/**
	 * Action service beans
	 */
	protected OccurrenceAction occurrenceAction = null;

	protected ProviderAction providerAction = null;

	protected ResourceAction resourceAction = null;

	protected TaxonAction taxonAction = null;

	protected NetworkAction networkAction = null;

	protected DensityAction densityAction = null;

	
	protected TemplateUtils templateUtils;
	
	protected PathMapping pathMapping;
	
	/**
	 * @param networkAction
	 *            the networkAction to set
	 */
	public void setNetworkAction(NetworkAction networkAction) {
		this.networkAction = networkAction;
	}

	/**
	 * @param occurrenceAction
	 *            the occurrenceAction to set
	 */
	public void setOccurrenceAction(OccurrenceAction occurrenceAction) {
		this.occurrenceAction = occurrenceAction;
	}

	/**
	 * @param taxonAction
	 *            the taxonAction to set
	 */
	public void setTaxonAction(TaxonAction taxonAction) {
		this.taxonAction = taxonAction;
	}

	public ModelAndView handleRequest(HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		Map<String, Object> params = new HashMap<String,Object>();
		Map<String, Object> results = new HashMap<String, Object>();
		String template = "";
		int countRecords = 0;

		int requestType = 0;
		

		VelocityContext velocity = new VelocityContext();

		String contentType = "text/xml; charset=UTF-8";
		response.setContentType(contentType);
		
		
		try {
			if (request.getMethod().equals("POST"))
				params = processPost(request);
			else
				params = processGet(request);

			if (params.get("service") == null)
				throw new GbifWebServiceException(
						"SERVICE (Occurrence/Taxon/Network/WFS) should be provided");
			
			String format = (String) params.get("format");
			
			switch (getRequestType((String) params.get("service"))) {
			
			//for an occurrence service request
			case OCCURRENCE_SERVICE:
				
				OccurrenceParameters occurrenceParameters = new OccurrenceParameters(params, pathMapping);
				requestType = occurrenceParameters.getRequestType();
				
				template = occurrenceAction.getTemplate(params);
				
				if(requestType == Action.LIST)
					results = occurrenceAction.findOccurrenceRecords(occurrenceParameters);
				else if(requestType == Action.GET)
					results = occurrenceAction.getOccurrenceRecord(occurrenceParameters);
				else if(requestType == Action.COUNT)
					results = occurrenceAction.countOccurrenceRecords(occurrenceParameters);
				else if(requestType == Action.HELP)
					results = occurrenceAction.returnHelpPage(occurrenceParameters);
				else if(requestType == Action.STYLESHEET)
				{
					response.getWriter().write(occurrenceParameters.getStylesheetResource(false));
					return null;
				}
				else if(requestType == Action.SCHEMA)
				{
					response.getWriter().write(occurrenceParameters.getSchemaResource(false));
					return null;
				}
												
				break;

			//for a taxon service request
			case TAXON_SERVICE:
				
				TaxonParameters taxonParameters = new TaxonParameters(params, pathMapping);
				requestType = taxonParameters.getRequestType();			
				
				template = taxonAction.getTemplate(params);
				
				if(requestType==Action.LIST)
					results = taxonAction.findTaxonRecords(taxonParameters);
				else if(requestType==Action.GET)
					results = taxonAction.getTaxonRecord(taxonParameters);
				else if(requestType==Action.COUNT)
					results = taxonAction.countTaxonRecords(taxonParameters);
				else if(requestType == Action.HELP)
					results = taxonAction.returnHelpPage(taxonParameters);				
				else if(requestType==Action.STYLESHEET)
				{
					response.getWriter().write(taxonParameters.getStylesheetResource(false));
					return null;
				}
				else if(requestType==Action.SCHEMA)
				{
					response.getWriter().write(taxonParameters.getSchemaResource(false));
					return null;
				}
				
				
								
				break;
				
			//for a density service request
			case DENSITY_SERVICE:
				
				DensityParameters densityParameters = new DensityParameters(params, pathMapping);
				requestType = densityParameters.getRequestType();		
				
				template = densityAction.getTemplate(params);
				
				if(requestType==Action.LIST)
					results = densityAction.findDensityRecords(densityParameters);
				else if(requestType == Action.HELP)
					results = densityAction.returnHelpPage(densityParameters);		
				else if(requestType==Action.STYLESHEET)
				{
					response.getWriter().write(densityParameters.getStylesheetResource(false));
					return null;
				}
				else if(requestType==Action.SCHEMA)
				{
					response.getWriter().write(densityParameters.getSchemaResource(false));
					return null;
				}
				
				break;
				
			//for a network service request
			case NETWORK_SERVICE:
				
				NetworkParameters networkParameters = new NetworkParameters(params, pathMapping);
				requestType = networkParameters.getRequestType();	
				
				template = networkAction.getTemplate(params);
				
				if(requestType==Action.LIST)
					results = networkAction.findNetworkRecords(networkParameters);
				else if (requestType==Action.GET)
					results = networkAction.getNetworkRecord(networkParameters);
				else if(requestType==Action.COUNT)
					results = networkAction.countNetworkRecords(networkParameters);
				else if(requestType == Action.HELP)
					results = networkAction.returnHelpPage(networkParameters);					
				else if(requestType==Action.STYLESHEET)
				{
					response.getWriter().write(networkParameters.getStylesheetResource(false));
					return null;
				}
				else if(requestType==Action.SCHEMA)
				{
					response.getWriter().write(networkParameters.getSchemaResource(false));
					return null;
				}
				
				break;
			case PROVIDER_SERVICE:
				
				ProviderParameters providerParameters = new ProviderParameters(params,pathMapping);
				requestType = providerParameters.getRequestType();
				
				template = providerAction.getTemplate(params);
				
				if(requestType==Action.LIST)
					results = providerAction.findProviderRecords(providerParameters);
				else if(requestType==Action.GET)
					results = providerAction.getProviderRecord(providerParameters);
				else if(requestType==Action.COUNT)
					results = providerAction.countProviderRecords(providerParameters);
				else if(requestType == Action.HELP)
					results = providerAction.returnHelpPage(providerParameters);					
				else if(requestType==Action.STYLESHEET)
				{
					response.getWriter().write(providerParameters.getStylesheetResource(false));
					return null;
				}
				else if(requestType==Action.SCHEMA)
				{
					response.getWriter().write(providerParameters.getSchemaResource(false));
					return null;
				}
				
				break;
			case RESOURCE_SERVICE:
				
				ResourceParameters resourceParameters = new ResourceParameters(params,pathMapping);
				requestType = resourceParameters.getRequestType();
				
				template = resourceAction.getTemplate(params);
				
				if(requestType==Action.LIST)
					results = resourceAction.findResourceRecords(resourceParameters);
				else if(requestType==Action.GET)
					results = resourceAction.getResourceRecord(resourceParameters);
				else if(requestType==Action.COUNT)
					results = resourceAction.countResourceRecords(resourceParameters);
				else if(requestType == Action.HELP)
					results = resourceAction.returnHelpPage(resourceParameters);					
				else if(requestType==Action.STYLESHEET)
				{
					response.getWriter().write(resourceParameters.getStylesheetResource(false));
					return null;
				}
				else if(requestType==Action.SCHEMA)
				{
					response.getWriter().write(resourceParameters.getSchemaResource(false));
					return null;
				}
				
				break;
				
			default:
				throw new GbifWebServiceException(
						"Unknown service parameter should be Occurrence/Taxonomy/Network/WFS");
			}
			
			
			if (format != null) {
				if (format.equalsIgnoreCase("kml")) {
					contentType = "application/vnd.google-earth.kml+xml";
					response.setContentType(contentType);
					//template="org/gbif/portal/ws/kml/kml.vm";
				}
			}
			//response.getWriter().write(results);		
			//results.save(response.getOutputStream(), xmlOptions);

		} catch (GbifWebServiceException gwsex) {
			template = "org/gbif/portal/ws/error.vm";
			velocity.put("errorMessage", gwsex.getMessage());
			velocity.put("service", params.get("service"));
			velocity.put("request", params.get("request"));
		}
		catch (Exception gwsex) {
			template = "org/gbif/portal/ws/error.vm";
			velocity.put("errorMessage", gwsex.toString());
			velocity.put("service", params.get("service"));
			velocity.put("request", params.get("request"));
		}
		
		/*if(results instanceof String) { //schema or stylesheet being returned
			response.getWriter().write(results.toString());
			return null;
		}*/
		
		//extract the portal's root and the WS' root URLs and place them in Velocity's context
		String url = request.getRequestURL().toString();
		String servletPath = request.getServletPath();
		String urlBase = url.substring(0, url.indexOf(servletPath));
		velocity.put("portalroot", urlBase);
		velocity.put("wsroot", urlBase + servletPath);	
		
		//place the results and count in Velocity's context
		velocity.put("response", results);		
		velocity.put("count", results.get("count"));
		
		Template templateObject = templateUtils.getTemplate(template);
		templateUtils.merge(templateObject, velocity, response.getWriter());
		return null;
	}

	public void setPathMapping(PathMapping pathMapping) {
		this.pathMapping = pathMapping;
	}

	public TemplateUtils getTemplateUtils() {
		return templateUtils;
	}

	public void setTemplateUtils(TemplateUtils templateUtils) {
		this.templateUtils = templateUtils;
	}

	/**
	 * Generate a cleaned parameters map from a get request, including any attributes (from URL rewriting)
	 * 
	 * @param request
	 * @return parameters map
	 * @throws GbifWebServiceException
	 */
	protected Map<String, Object> processGet(HttpServletRequest request)
			throws GbifWebServiceException {

		Map<String, Object> kvps = new HashMap<String, Object>();
		
		Enumeration enumeration = request.getParameterNames();

		// Place the first value in the map under its own name (since in general
		// most parameters are processed as singletons, but if there are more
		// than one values, also insert the array under the name "<name>_array".
		if (enumeration != null) {
			Map parameterMap = request.getParameterMap();
			while (enumeration.hasMoreElements()) {
				String name = (String) enumeration.nextElement();
				String[] values = (String[]) parameterMap.get(name);
				kvps.put(name.toLowerCase(), values[0]);
				if (values.length > 1) {
					kvps.put(name + ARRAY_SUFFIX, values);
				}
			}
		}
		
		enumeration = request.getAttributeNames();

		if (enumeration != null) {
			while (enumeration.hasMoreElements()) {
				String name = (String) enumeration.nextElement();
				Object value = request.getAttribute(name);
				if (value instanceof String) {
					kvps.put(name.toLowerCase(), (String) value);
				}
			}
		}
		
		String url = request.getRequestURL().toString();
		String servletPath = request.getServletPath();
		String urlBase = url.substring(0, url.indexOf(servletPath));
		kvps.put("portalroot", urlBase);
		kvps.put("wsroot", urlBase + servletPath);

		log.debug("Parameter map: " + kvps.toString());
		return kvps;
	}

	/**
	 * Generate a cleaned parameters map from a POST request (mainly for WFS XML
	 * requests)
	 * 
	 * @param request
	 * @return parameters map
	 */
	protected Map<String, Object> processPost(HttpServletRequest request) {
		Map<String, Object> params = new HashMap<String, Object>();
		return params;
	}

	/**
	 * Returns the request type for a given KVP set.
	 */
	private int getRequestType(String request) {
		log.debug("dispatcher got request " + request);

		if (request != null) {
			request = request.toUpperCase();

			if (request.equals("OCCURRENCE"))
				return Dispatcher.OCCURRENCE_SERVICE;
			else if (request.equals("PROVIDER"))
				return Dispatcher.PROVIDER_SERVICE;
			else if (request.equals("RESOURCE"))
				return Dispatcher.RESOURCE_SERVICE;
			else if (request.equals("TAXON"))
				return Dispatcher.TAXON_SERVICE;
			else if (request.equals("NETWORK"))
				return Dispatcher.NETWORK_SERVICE;
			else if (request.equals("DENSITY"))
				return Dispatcher.DENSITY_SERVICE;
			else if (request.equals("WFS"))
				return Dispatcher.NETWORK_SERVICE;
			else
				return Dispatcher.UNKNOWN_SERVICE;
		} else
			return Dispatcher.UNKNOWN_SERVICE;
	}

	/**
	 * @param providerAction the providerAction to set
	 */
	public void setProviderAction(ProviderAction providerAction) {
		this.providerAction = providerAction;
	}

	/**
	 * @param resourceAction the resourceAction to set
	 */
	public void setResourceAction(ResourceAction resourceAction) {
		this.resourceAction = resourceAction;
	}

	/**
	 * @param densityAction the densityAction to set
	 */
	public void setDensityAction(DensityAction densityAction) {
		this.densityAction = densityAction;
	}

}
